﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Jama;
using Jama.SVD;

namespace ExtremeLearningMachine
{
    public class ELM
    {
        #region Constructor(s)
        public ELM(ELMConfiguration config)
            : this (config.Prov, config.HidenNodesNumber, config.Seed, config.ActivationFunctionType)
        {
        }
        public ELM (DataProvider provider, long hidenNodesNumber, int seed, EActivationFunctionType activationFunctionType)
        {
            _DataProvider = provider;
            _ActivationFunctionType = activationFunctionType;
            Ñ = hidenNodesNumber;
            _Seed = seed;
            _Random = new Random(_Seed);

            _Initialize();
        }

        public ELM(DataProvider provider, long hidenNodesNumber, int seed, EActivationFunctionType activationFunctionType, double[][] B, double[][] W)
        {
            _DataProvider = provider;
            _ActivationFunctionType = activationFunctionType;
            Ñ = hidenNodesNumber;
            _Seed = seed;
            _Random = new Random(seed);

            this.B = B;
            this.w = W;

            _InitializeForPrediction();
        }


        #endregion

        #region Attributes
        /// <summary>
        /// Matriz com as entradas de treinamento.
        /// Tamanho Nxn
        /// </summary>
        private double[][] x = null;
        /// <summary>
        /// Matriz com as saídas de treinamento.
        /// Tamanho Nxm
        /// </summary>
        private double[][] T = null;

        /// <summary>
        /// Matriz de pesos resultante.
        /// Tamanho Ñxm
        /// </summary>
        private double[][] B = null;

        /// <summary>
        /// Matriz de saída da camada escondida.
        /// Tamanho NxÑ
        /// </summary>
        private double[][] H = null;

        /// <summary>
        /// Matriz de pesos da camada de entrada.
        /// Tamanho Ñxn.
        /// </summary>
        private double[][] w = null;

        /// <summary>
        /// Bias da camada escondida.
        /// Tamanho Ñ.
        /// </summary>
        private double[] b = null;

        /// <summary>
        /// Número de exemplos de treinamento.
        /// </summary>
        private long N = -1;

        /// <summary>
        /// Número de neurônios da camada escondida.
        /// </summary>
        private long Ñ = -1;

        /// <summary>
        /// Número de neurônios da camada de entrada.
        /// </summary>
        private long n = -1;

        /// <summary>
        /// Número de neurônios da camda de saída.
        /// </summary>
        private long m = -1;

        #region Configuration
        private EActivationFunctionType _ActivationFunctionType;
        private DataProvider _DataProvider = null;
        private int _Seed = -1;
        private Random _Random = null;
        #endregion
        #endregion

        #region Properties
        /// <summary>
        /// Matriz de pesos resultante.
        /// </summary>
        public double[][] GetB { get { return B; } }

        /// <summary>
        /// Matriz de pesos da camada de entrada.
        /// </summary>
        public double[][] GetW { get { return w; } }

        public long HiddenNodesNumber { get { return this.Ñ; } }

        public double[][] GetT { get { return this.T; } }
        #endregion

        public EDataType DataType { get { return _DataProvider.DataType; } }

        #region Private Methods
        /// <summary>
        /// Inicializa as variáveis da rede.
        /// </summary>
        private void _Initialize()
        {
            Data traindData = _DataProvider.TrainSet[0];
            n = traindData.Input.Length;
            m = traindData.Output.Length;
            N = _DataProvider.TrainSet.Length;
            b = new double[Ñ];
            w = w.InitializeMatrix(Ñ, n);
            x = x.InitializeMatrix(N, n);
            T = w.InitializeMatrix(N, m);
            H = H.InitializeMatrix(N, Ñ);

            _GenerateTMatrix();
            _GenerateXMatrix();
        }

        private void _InitializeForPrediction()
        {
            Data data = _DataProvider.DataSet[0];
            n = data.Input.Length;
            m = data.Output.Length;
            N = _DataProvider.DataSet.Length;
            b = new double[Ñ];

            for (long i = 0; i < Ñ; i++)
                b[i] = 1;

            x = x.InitializeMatrix(N, n);
            T = w.InitializeMatrix(N, m);
            H = H.InitializeMatrix(N, Ñ);

            _GenerateXMatrixForPrediction();
            _CalculateHMatrix();
            _CalculateTUsingJama();
        }

        /// <summary>
        /// Gera a matrix T a partir da base de dados.
        /// </summary>
        private void _GenerateTMatrix()
        {
            for (long i = 0; i < N; i++)
            {
                for (long j = 0; j < m; j++)
                {
                    T[i][j] = _DataProvider.TrainSet[i].Output[j];
                }
            }
        }

        private void _GenerateXMatrix()
        {
            for (long i = 0; i < N ; i++)
            {
                for (long j = 0 ; j < n; j++)
                {
                    x[i][j] = _DataProvider.TrainSet[i].Input[j];
                }
            }
        }

        private void _GenerateXMatrixForValidation()
        {
            for (long i = 0; i < N; i++)
            {
                for (long j = 0; j < n; j++)
                {
                    x[i][j] = _DataProvider.ValidationSet[i].Input[j];
                }
            }
        }

        private void _GenerateXMatrixForPrediction()
        {
            for (long i = 0; i < N; i++)
            {
                for (long j = 0; j < n; j++)
                {
                    x[i][j] = _DataProvider.DataSet[i].Input[j];
                }
            }
        }

        /// <summary>
        /// Gera valores aleatórios para as matrizes de peso de entrada
        /// </summary>
        private void _RandomlyAssignInputWeights()
        {
            double rand = 0;

            for (long i = 0; i < Ñ; i++)
            {              
                b[i] = 1;

                for (long j = 0; j < n; j++)
                {
                    rand = _Random.NextDouble();

                    if (_Random.NextDouble() <= 0.5)
                        rand = rand * 10;

                    if (_Random.NextDouble() <= 0.5)
                        rand = - (rand);

                    w[i][j] = rand;
                }
            }
        }

        private void _CalculateHMatrix()
        {
            for (int i = 0; i < N; i++)
            {
                for (int j = 0; j < Ñ; j++)
                {
                    H[i][j] = _G(w[j].InnerProduct(x[i]) + b[j]);
                }
            }
        }

        /// <summary>
        /// Função de ativação
        /// </summary>
        private double _G(double value)
        {
            double result = 0;

            switch (_ActivationFunctionType)
            {
                case EActivationFunctionType.Linear:
                    result = value;
                    break;
                case EActivationFunctionType.SigmoidLogistic:
                    result = 1 / (1 + Math.Exp(-value));
                    break;
                case EActivationFunctionType.HyperbolicTangent:
                    result = (Math.Exp(2 * value) - 1) / (Math.Exp(2 * value) + 1);
                    break;
            }
            return result;
        }

        /// <summary>
        /// Calcula a Matriz de pesos B utilizando a biblioteca jama
        /// B = H*.T
        /// </summary>
        private void _CalculateBUsingJama()
        {
            Matrix h = new Matrix(H, (int)N, (int)Ñ);
            SingularValueDecomposition svd = h.svd();

            Matrix V = svd.getV();
            Matrix S = svd.getS();
            Matrix U = svd.getU();

            Matrix U_T = U.transpose();

            Matrix S_T = S.transpose();
            Matrix S_TS = S_T.times(S);
            Matrix S_TS_1 = S_TS.inverse();

            Matrix inversa = V.times(S_TS_1).times(S_T).times(U_T);
            Matrix T = new Matrix((double[][]) this.T.Clone(), (int)N, (int)m);

            Matrix B = inversa.times(T);
            this.B = B.getArray();

            //Matrix h = new Matrix(H, (int)N, (int)Ñ);

            //Matrix inversa = h.PInverse(h.transpose()); ;
            //Matrix T = new Matrix(this.T, (int)N, (int)m);

            //Matrix B = inversa.times(T.transpose());
            //this.B = B.getArray();
        }
        
        /// <summary>
        /// Calcula a Matriz T utilizando a biblioteca jama
        /// T = HB;
        /// </summary>
        private void _CalculateTUsingJama()
        {
            Matrix h = new Matrix(H, (int)N, (int)Ñ);
            Matrix b = new Matrix(B, (int)Ñ, (int)m);

            Matrix T = null;
            this.T = null;
            T = h.times(b);
            this.T = T.getArray();
        }
        #endregion

        #region Public Methods
        public void SetB(double[][] b)
        {
            this.B = b;
        }
        public void SetW(double[][] w)
        {
            this.w = w;
        }
        public void Train()
        {
            _RandomlyAssignInputWeights();
            _CalculateHMatrix();
            _CalculateBUsingJama();

            _CalculateTUsingJama();
        }

        public void Validate()
        {
            Data validateData = _DataProvider.ValidationSet[0];
            N = _DataProvider.ValidationSet.Length;
            x = x.InitializeMatrix(N, n);
            H = H.InitializeMatrix(N, Ñ);

            _GenerateXMatrixForValidation();
            _CalculateHMatrix();
            _CalculateTUsingJama();
        }
        #region Performance Evaluation
        public double GenerateEPMAForTrain()
        {
            double EPMA = 0;
            double Error = 0;
            for (int i = 0; i < N; i++)
            {
                for (int j = 0; j < this.m; j++)
                {                    
                    Error = _DataProvider.TrainSet[i].Output[j] - T[i][j];

                    double baixo = _DataProvider.TrainSet[i].Output[j] == 0 ? 0.000000001 : _DataProvider.TrainSet[i].Output[j];

                    EPMA += Math.Abs(Error) / baixo ;
                    Error = 0;
                }
            }

            EPMA = EPMA / N;
            return EPMA * 100;
        }
        public double GenerateEPMAForValidation()
        {
            double EPMA = 0;
            double Error = 0;
            for (int i = 0; i < N; i++)
            {
                for (int j = 0; j < this.m; j++)
                {
                    Error = _DataProvider.ValidationSet[i].Output[j] - T[i][j];

                    double baixo = _DataProvider.TrainSet[i].Output[j] == 0 ? 0.000000001 : _DataProvider.TrainSet[i].Output[j];

                    EPMA += Math.Abs(Error) / baixo;
                }
            }

            EPMA = EPMA / N;
            return EPMA * 100;
        }
                
        public double GenerateEMQForTrain()
        {
            double EMQ = 0;
            double Error = 0;
            for (int i = 0; i < N; i++)
            {
                for (int j = 0; j < this.m; j++)
                {
                    Error = _DataProvider.TrainSet[i].Output[j] - T[i][j];

                    EMQ += Math.Pow(Error, 2);
                    Error = 0;
                }
            }

            EMQ = EMQ / N;
            return EMQ;
        }
        public double GenerateEMQForValidation()
        {
            double EMQ = 0;
            double Error = 0;
            for (int i = 0; i < N; i++)
            {
                for (int j = 0; j < this.m; j++)
                {
                    Error = _DataProvider.ValidationSet[i].Output[j] - T[i][j];

                    EMQ += Math.Pow(Error, 2);
                    Error = 0;
                }
            }

            EMQ = EMQ / N;
            return EMQ;
        }

        public double GenerateRMSEForTrain()
        {
            return Math.Sqrt(GenerateEMQForTrain());
        }
        public double GenerateRMSEForValidation()
        {
            return Math.Sqrt(GenerateEMQForValidation());
        }

        public double GenerateSRForTrain()
        {
            double SuccessCount = 0;
            double Error = -1;
            for (int i = 0; i < N; i++)
            {
                Error = _DataProvider.TrainSet[i].Output.Compare(T[i]);

                if (Error == 0)
                    SuccessCount++;

                Error = -1;
            }

            double SR = SuccessCount * 100 / N;
            return SR;
        }
        public double GenerateSRForValidation()
        {
            double SuccessCount = 0;
            double Error = -1;
            for (int i = 0; i < N; i++)
            {
                Error = _DataProvider.ValidationSet[i].Output.Compare(T[i]);
                    
                if (Error == 0)
                    SuccessCount++;

                Error = -1;
            }

            double x = SuccessCount * 100 / N;
            return x;
        }

        public double GenerateDEVForTrain()
        {
            double Mean = 0;
            double Error = 0;

            for (int i = 0; i < N; i++)
            {
                for (int j = 0; j < this.m; j++)
                {
                    Error = _DataProvider.TrainSet[i].Output[j] - T[i][j];

                    Mean += Error;

                    Error = 0;
                }
            }

            Mean = Mean / N;

            double DEV = 0;
            for (int i = 0; i < N; i++)
            {
                for (int j = 0; j < this.m; j++)
                {
                    Error = _DataProvider.TrainSet[i].Output[j] - T[i][j];

                    DEV += Math.Pow(Error - Mean, 2);

                    Error = 0;
                }
            }

            DEV = DEV / (N - 1);
            return DEV;
        }
        public double GenerateDEVForValidation()
        {
            double Mean = 0;
            double Error = 0;

            for (int i = 0; i < N; i++)
            {
                for (int j = 0; j < this.m; j++)
                {
                    Error = _DataProvider.ValidationSet[i].Output[j] - T[i][j];
                    
                    Mean += Error;

                    Error = 0;
                }
            }

            Mean = Mean / N;

            double DEV = 0;
            for (int i = 0; i < N; i++)
            {
                for (int j = 0; j < this.m; j++)
                {
                    Error = _DataProvider.ValidationSet[i].Output[j] - T[i][j];

                    DEV += Math.Pow(Error - Mean, 2);

                    Error = 0;
                }
            }

            DEV = DEV / (N - 1);
            return DEV;
        }
        #endregion
        #endregion
    }

    #region Auxiliar Classes
    public enum EActivationFunctionType
    {
        /// <summary>
        /// Linear
        /// </summary>
        Linear = 1,

        /// <summary>
        /// Sigmoide Logistica
        /// </summary>
        SigmoidLogistic = 2,

        /// <summary>
        /// Tangente Hiperbolica
        /// </summary>
        HyperbolicTangent = 3
    }

    [Flags]
    public enum EPerformanceInfo
    {
        None = 0,

        EMQ = 0x1,

        RMSE = 0x2,
       
        SR = 0x4,

        EPMA = 0x8,

        DEV = 0x10
    }
    #endregion
}
